/*
 *
 *  Copyright (C) 2010 Tal Eisenberg
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package tal.drivechain.module.properties;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.StringTokenizer;
import org.w3c.dom.NodeList;
import tal.drivechain.chain.ChainNode;
import tal.drivechain.variable.VarTable;
import tal.drivechain.variable.types.DataType;
import tal.drivechain.variable.expressions.SimpleExpression;
import tal.drivechain.variable.Variable;

/**
 *
 * @author Main
 */
public class VarParser implements PropertyParser {
    public enum ParseMethod {
        NAMED, UNNAMED, VALUE, NAMEONLY, FLATVALUE;

        public static ParseMethod getParseMethod(String method) throws IllegalArgumentException {
            if (method.equals("named")) return NAMED;
            else if (method.equals("unnamed")) return UNNAMED;
            else if (method.equals("value")) return VALUE;
            else if (method.equals("name-only")) return NAMEONLY;
            else if (method.equals("flat-value")) return FLATVALUE;
            else throw new IllegalArgumentException("Invalid parse-method: " + method);
        }
    }

    private boolean doCheckName;
    private List<DataType> enforceTypes = new ArrayList<DataType>();
    private DataType varType;
    private String defaultName;
    private ParseMethod method;

    public void setEnforceTypes(Collection<DataType> enforceTypes) {
        this.enforceTypes.clear();
        this.enforceTypes.addAll(enforceTypes);
    }

    public void setDataType(DataType varType) {
        this.varType = varType;
    }

    public void setDefaultName(String defaultName) {
        this.defaultName = defaultName;
    }

    public void setParseMethod(ParseMethod method) {
        this.method = method;
    }

    private String allowedTypes(List<DataType> enforceTypes) {
        if (enforceTypes.size()==0) return "";
        String ret = "";
        for (DataType type : enforceTypes) {
            ret += type.getName() + ", ";
        }
        return ret.substring(0, ret.length()-2);
    }

    private boolean checkName(String name) {
        if (doCheckName && name!=null) {
            return VarTable.isValidVarName(name);
        } else return true;
    }

    private boolean checkName(Variable var) {
        return checkName(var.getName());
    }

    private boolean checkType(Variable var) {
        if (enforceTypes.size()==0) return true;
        else {
            DataType vType;
            if (var.getDataType().matches(DataType.EXPR))
                vType = ((SimpleExpression)var.getInternalValue()).getExprReturnType();
            else vType = var.getDataType();
                
            for (DataType type : enforceTypes) {
                if (type.matches(vType))
                    return true;
            }
            return false;
        }
    }

    public String propToString(Object property, ChainNode context) {
        if (property==null) return "null";
        switch (method) {
            case NAMED:
                return ((Variable)property).toString();
            case UNNAMED:
                Variable var = (Variable)property;
                return "(" + (var.isEvaluateAtHome()?"":"~") + var.getDataType().getName() + ":" + ((Variable)property).getDataType().toString(((Variable)property).getInternalValue(), context) + ")";
            case VALUE:
                return ((Variable)property).getDataType().toString(((Variable)property).getValue(null), null);
            case FLATVALUE:
                return varType.toString(property, null);
            case NAMEONLY:
                return ((Variable)property).getName();
            default:
                throw new IllegalArgumentException("Unknown parse method: " + method.name());
        }
    }
            
    public void setAttribures(HashMap<String, String> attributes, NodeList childElements) throws IllegalArgumentException {
        // parse-method="variable|definition|value|name|" default-type="DATATYPE" enforce-types default-name="STRING"
        String strParseMethod = attributes.get("parse-method");
        if (strParseMethod==null)
            throw new IllegalArgumentException("Missing parse-method attribute from var-parser element.");        
        setParseMethod(ParseMethod.getParseMethod(strParseMethod));
        if (method==ParseMethod.NAMED || method==ParseMethod.UNNAMED) {
            String strEnforceTypes = attributes.get("enforce-types");
            if (strEnforceTypes!=null) {
                StringTokenizer types = new StringTokenizer(strEnforceTypes, ",");
                while (types.hasMoreTokens()) {
                    String strType = types.nextToken();
                    DataType dataType = DataType.parseType(strType);
                    enforceTypes.add(dataType);
                }
            }
        } 
        
        String strDefaultType = attributes.get("default-type");
        if (strDefaultType!=null) 
            varType = DataType.parseType(strDefaultType);
        else varType = null;
        
        defaultName = attributes.get("default-name");
        if (defaultName==null) defaultName = attributes.get("property-name");
        if (defaultName==null) defaultName = "aVar";

        if (attributes.containsKey("check-name") &&
                attributes.get("check-name").equals("true"))
            doCheckName = true;
        
        if (!checkName(defaultName)) {
            throw new IllegalArgumentException("Illegal default variable name: " + defaultName);
        }
    }

    public Object parseProperty(String prop) throws IllegalArgumentException {
        switch (method) {
            case NAMED: // can be anything if defaultName has a value. if no defaultName
                Variable var = Variable.parseVariable(defaultName, varType, prop, false);
                if (checkType(var) && checkName(var)) return var;
                else throw new IllegalArgumentException("Invalid data type: " + var.getDataType().getName() + " (expected: " + allowedTypes(enforceTypes) + ")");
            case UNNAMED: // can be either (type:value) or value
                Variable def = Variable.parseVariable(defaultName, varType, prop, true);
                if (checkType(def) && checkName(def)) return def;
                else throw new IllegalArgumentException("Invalid data type: " + def.getDataType().getName() + " (expected: " + allowedTypes(enforceTypes) + ")");
            case VALUE: // can be only value
                if (varType==null) throw new IllegalArgumentException("a parse-method of value must have a default-type attribute");
                return Variable.parseVarValue(defaultName, varType, prop);
            case FLATVALUE: // can be only value
                if (varType==null) throw new IllegalArgumentException("a parse-method of value must have a default-type attribute");
                return Variable.parseVarValue(defaultName, varType, prop).getValue(null);
            case NAMEONLY:
                return new Variable(prop, varType, null);
            default:
                throw new IllegalArgumentException("Program error: Invalid parse-method enumeration!");
        }
    }

    public String getDescription() {
        String strMethod;
        switch (method) {
            case NAMED:
                strMethod = "named or unnamed variable definition";
                break;
            case UNNAMED:
                strMethod = "data type and value";
                break;
            case VALUE:
                strMethod = "value only";
                break;
            case NAMEONLY:
                strMethod = "name only";
                break;
            default:
                strMethod = "?";
        }
        String strType = "";
        if (varType!=null) strType = varType.getName();
        return "Variable - " + strMethod + " (" + strType + ")";
    }

    public String getPropertyType() {
        switch (method) {
            case NAMED:
                return "variable";
            case UNNAMED:
                return "unnamed variable";
            case VALUE:
            case FLATVALUE:
                return varType.getName();
            case NAMEONLY:
                return "variable name";
            default: 
                return "?";
        }
    }
}
